/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright Red Hat Inc. and Hibernate Authors
 */
package org.hibernate.engine.jdbc.internal;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.hibernate.AssertionFailure;
import org.hibernate.ScrollMode;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.jdbc.spi.SqlExceptionHelper;
import org.hibernate.engine.jdbc.spi.StatementPreparer;
import org.hibernate.resource.jdbc.spi.JdbcSessionContext;
import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;

import org.checkerframework.checker.nullness.qual.Nullable;

import static org.hibernate.engine.jdbc.JdbcLogging.JDBC_LOGGER;

/**
 * Standard implementation of {@link StatementPreparer}.
 *
 * @author Steve Ebersole
 * @author Lukasz Antoniak
 * @author Brett Meyer
*/
@SuppressWarnings("resource")
class StatementPreparerImpl implements StatementPreparer {
	private final JdbcCoordinatorImpl jdbcCoordinator;
	private final JdbcServices jdbcServices;

	/**
	 * Construct a StatementPreparerImpl
	 *
	 * @param jdbcCoordinator The JdbcCoordinatorImpl
	 */
	StatementPreparerImpl(JdbcCoordinatorImpl jdbcCoordinator, JdbcServices jdbcServices) {
		this.jdbcCoordinator = jdbcCoordinator;
		this.jdbcServices = jdbcServices;
	}

	protected final JdbcSessionContext settings() {
		return jdbcCoordinator.getJdbcSessionOwner().getJdbcSessionContext();
	}

	protected final Connection connection() {
		return logicalConnection().getPhysicalConnection();
	}

	protected final LogicalConnectionImplementor logicalConnection() {
		return jdbcCoordinator.getLogicalConnection();
	}

	protected final SqlExceptionHelper sqlExceptionHelper() {
		return jdbcServices.getSqlExceptionHelper();
	}

	@Override
	public Statement createStatement() {
		try {
			final var statement = connection().createStatement();
			jdbcCoordinator.getLogicalConnection().getResourceRegistry().register( statement, true );
			return statement;
		}
		catch ( SQLException e ) {
			throw sqlExceptionHelper().convert( e, "Could not create statement" );
		}
	}

	@Override
	public PreparedStatement prepareStatement(String sql) {
		return prepareStatement( sql, false );
	}

	@Override
	public PreparedStatement prepareStatement(String sql, final boolean isCallable) {
		jdbcCoordinator.executeBatch();
		return buildPreparedStatementPreparationTemplate( sql, isCallable ).prepareStatement();
	}

	@Override
	public CallableStatement prepareCallableStatement(String sql) {
		jdbcCoordinator.executeBatch();
		return (CallableStatement) prepareStatement( sql, true );
	}

	private StatementPreparationTemplate buildPreparedStatementPreparationTemplate(String sql, final boolean isCallable) {
		return new StatementPreparationTemplate( sql ) {
			@Override
			protected PreparedStatement doPrepare() throws SQLException {
				return isCallable
						? connection().prepareCall( sql )
						: connection().prepareStatement( sql );
			}
		};
	}

	private void checkAutoGeneratedKeysSupportEnabled() {
		if ( ! settings().isGetGeneratedKeysEnabled() ) {
			throw new AssertionFailure( "getGeneratedKeys() support is not enabled" );
		}
	}

	@Override
	public PreparedStatement prepareStatement(String sql, final int autoGeneratedKeys) {
		if ( autoGeneratedKeys == PreparedStatement.RETURN_GENERATED_KEYS ) {
			checkAutoGeneratedKeysSupportEnabled();
		}
		jdbcCoordinator.executeBatch();
		return new StatementPreparationTemplate( sql ) {
			public PreparedStatement doPrepare() throws SQLException {
				return connection().prepareStatement( sql, autoGeneratedKeys );
			}
		}.prepareStatement();
	}

	@Override
	public PreparedStatement prepareStatement(String sql, final String[] columnNames) {
		checkAutoGeneratedKeysSupportEnabled();
		jdbcCoordinator.executeBatch();
		return new StatementPreparationTemplate( sql ) {
			public PreparedStatement doPrepare() throws SQLException {
				return connection().prepareStatement( sql, columnNames );
			}
		}.prepareStatement();
	}

	@Override
	public PreparedStatement prepareQueryStatement(
			String sql,
			boolean isCallable,
			@Nullable ScrollMode scrollMode) {
		final int resultSetType;
		if ( scrollMode != null && scrollMode != ScrollMode.FORWARD_ONLY ) {
			if ( !settings().isScrollableResultSetsEnabled() ) {
				throw new AssertionFailure( "Scrollable result sets are not enabled" );
			}
			resultSetType = scrollMode.toResultSetType();
		}
		else {
			resultSetType = ResultSet.TYPE_FORWARD_ONLY;
		}

		final var preparedStatement =
				new QueryStatementPreparationTemplate( sql ) {
					public PreparedStatement doPrepare() throws SQLException {
						return isCallable
								? connection().prepareCall( sql, resultSetType, ResultSet.CONCUR_READ_ONLY )
								: connection().prepareStatement( sql, resultSetType, ResultSet.CONCUR_READ_ONLY );
					}
				}.prepareStatement();
		jdbcCoordinator.registerLastQuery( preparedStatement );
		return preparedStatement;
	}

	private abstract class StatementPreparationTemplate {
		protected final String sql;

		protected StatementPreparationTemplate(String incomingSql) {
			final String inspectedSql =
					jdbcCoordinator.getJdbcSessionOwner().getJdbcSessionContext()
							.getStatementInspector().inspect( incomingSql );
			sql = inspectedSql == null ? incomingSql : inspectedSql;
		}

		public PreparedStatement prepareStatement() {
			try {
				jdbcServices.getSqlStatementLogger().logStatement( sql );

				final var jdbcSessionOwner = jdbcCoordinator.getJdbcSessionOwner();
				final var observer = jdbcSessionOwner.getJdbcSessionContext().getEventHandler();
				final var eventMonitor = jdbcSessionOwner.getEventMonitor();
				final var jdbcPreparedStatementCreation = eventMonitor.beginJdbcPreparedStatementCreationEvent();
				final PreparedStatement preparedStatement;
				try {
					observer.jdbcPrepareStatementStart();
					preparedStatement = doPrepare();
					setStatementTimeout( preparedStatement );
				}
				finally {
					eventMonitor.completeJdbcPreparedStatementCreationEvent( jdbcPreparedStatementCreation, sql );
					observer.jdbcPrepareStatementEnd();
				}
				postProcess( preparedStatement );
				return preparedStatement;
			}
			catch ( SQLException e ) {
				throw sqlExceptionHelper().convert( e, "Could not prepare statement", sql );
			}
		}

		protected abstract PreparedStatement doPrepare() throws SQLException;

		public void postProcess(PreparedStatement preparedStatement) throws SQLException {
			jdbcCoordinator.getLogicalConnection().getResourceRegistry().register( preparedStatement, true );
//			logicalConnection().notifyObserversStatementPrepared();
		}

		private void setStatementTimeout(PreparedStatement preparedStatement) throws SQLException {
			final int remainingTransactionTimeOutPeriod = jdbcCoordinator.determineRemainingTransactionTimeOutPeriod();
			if ( remainingTransactionTimeOutPeriod > 0 ) {
				preparedStatement.setQueryTimeout( remainingTransactionTimeOutPeriod );
			}
		}
	}

	private abstract class QueryStatementPreparationTemplate extends StatementPreparationTemplate {
		protected QueryStatementPreparationTemplate(String sql) {
			super( sql );
		}

		public void postProcess(PreparedStatement preparedStatement) throws SQLException {
			super.postProcess( preparedStatement );
			setStatementFetchSize( preparedStatement );
		}
	}

	private void setStatementFetchSize(PreparedStatement statement) throws SQLException {
		final Integer fetchSize = settings().getFetchSizeOrNull();
		if ( fetchSize != null ) {
			JDBC_LOGGER.settingFetchSize( fetchSize );
			statement.setFetchSize( fetchSize );
		}
		else {
			if ( JDBC_LOGGER.isDebugEnabled() ) {
				final int defaultFetchSize = statement.getFetchSize();
				if ( defaultFetchSize > 0 && defaultFetchSize < 100 ) {
					JDBC_LOGGER.lowFetchSize( defaultFetchSize );
				}
			}
			else if ( JDBC_LOGGER.isTraceEnabled() ) {
				JDBC_LOGGER.fetchSize( statement.getFetchSize() );
			}
		}
	}

}
